-- 
-- Please see the readme.txt file included with this distribution for 
-- attribution and copyright information.
--

-- NOTE: See manager_rules.lua for structures used to store power abilities

---- HELPER FUNCTIONS 

function getAbility(s)
	local statval = "";
	if s == "str" or s == "strength" then
		statval = "strength";
	elseif s == "con" or s == "constitution" then
		statval = "constitution";
	elseif s == "dex" or s == "dexterity" then
		statval = "dexterity";
	elseif s == "int" or s == "intelligence" then
		statval = "intelligence";
	elseif s == "wis" or s == "wisdom" then
		statval = "wisdom";
	elseif s == "cha" or s == "charisma" then
		statval = "charisma";
	end
	return statval;
end

function getDefense(s)
	local defense = "";
	if s == "ac" then
		defense = "ac";
	elseif s == "fort" or s == "fortitude" then
		defense = "fortitude";
	elseif s == "ref" or s == "reflex" then
		defense = "reflex";
	elseif s == "will" then
		defense = "will";
	end
	return defense;
end

function parseValuePhrase(words, index)
	-- SETUP
	local rValue = { nIndex = index, aDice = {}, aAbilities = {} };
	local sModifier = nil;

	-- ITERATE
	local j = index;
	while words[j] do
		-- LOCATE DICE AND ABILITY TERMS AND MODIFIERS
		local sTempAbility = "";
		if StringManager.isWord(words[j], {"your", "his", "her"}) then
			-- SKIP
		elseif StringManager.isWord(words[j], "or") then
			rValue.bOrFlag = true;
		elseif StringManager.isWord(words[j], "+") then
			if sModifier then
				break;
			end
		elseif StringManager.isWord(words[j], "twice") then
			if sModifier then
				break;
			end
			sModifier = "double";
		elseif StringManager.isWord(words[j], {"one-half", "half"}) then
			if sModifier then
				break;
			end
			sModifier = "half";
		elseif StringManager.isWord(words[j], "healing") and 
				StringManager.isWord(words[j+1], "surge") and StringManager.isWord(words[j+2], "value") then
			sTempAbility = "hsv";
			rValue.nIndex = j + 2;
			j = j + 2;
		elseif StringManager.isWord(words[j], "level") then
			sTempAbility = "level";
		elseif StringManager.isDiceString(words[j]) then
			if sModifier then
				break;
			end
			table.insert(rValue.aDice, words[j]);
			rValue.nIndex = j;
		else
			sTempAbility = PowersManager.getAbility(words[j]);
			if sTempAbility ~= "" then
				if StringManager.isWord(words[j+1], "modifier") then
					j = j + 1;
				end
			else
				break;
			end
		end
		
		-- ADD ABILITY TERMS
		if sTempAbility ~= "" then
			if sModifier then
				sTempAbility = sModifier .. sTempAbility;
				sModifier = nil;
			end
			
			table.insert(rValue.aAbilities, sTempAbility);
			rValue.nIndex = j;
		end
	
		-- INCREMENT
		j = j + 1;
	end
	
	-- CLEAR VALUE VARIABLE, IF NOTHING FOUND
	if (#(rValue.aDice) == 0) and (#(rValue.aAbilities) == 0) then
		rValue = nil;
	end
	
	-- RESULTS
	return rValue;
end

--
-- ATTACKS
--

-- Form [PHB]: <stat> vs. <defense>
-- Form [PHB]: <stat> + <bonus> vs. <defense>
-- Form [PHB]: <stat> - <penalty> vs. <defense>
-- Form [MM ]: +<bonus> vs. <defense>
-- Form [MM ]: -<penalty> vs. <defense>
function parseAttacks(words)
	-- Start with an empty set
	local attacks = {};
	local bAttackContinuationAllowed = false;

  	-- Iterate through the words looking for attack clauses
  	for i = 1, #words do
  		-- Our main trigger is the "vs" in the defense part of the clause
  		if StringManager.isWord(words[i], "vs") and i > 1 then

  			-- Calculate the attack stat, bonus and defense
  			local atk_starts = i;
  			
  			-- ONE OFF (PHB - Valiant Strike)
  			if StringManager.isWord(words[atk_starts-1], "you") and StringManager.isWord(words[atk_starts-2], "to") and
  					StringManager.isWord(words[atk_starts-3], "adjacent") and StringManager.isWord(words[atk_starts-4], "enemy") and
  					StringManager.isWord(words[atk_starts-5], "per") then
  				atk_starts = atk_starts - 5;
  			end
  			
  			local atk_stat = "";
  			local atk_bonus = tonumber(words[atk_starts-1]);
  			if atk_bonus then
  				atk_starts = atk_starts - 1;
  				
  				local temp_index = atk_starts - 1;
  				if StringManager.isWord(words[temp_index], "+") then
					temp_index = temp_index - 1;
				elseif StringManager.isWord(words[temp_index], "-") then
					atk_bonus = 0 - atk_bonus;
					temp_index = temp_index - 1;
				end
				
				atk_stat = getAbility(words[temp_index]);
				if atk_stat ~= "" then
					atk_starts = temp_index;
				end
  			else
  				atk_stat = getAbility(words[atk_starts-1]);
  				atk_bonus = 0;
  				atk_starts = atk_starts - 1;

  				-- ONE OFF (PHB - Eldritch Blast)
  				if StringManager.isWord(words[atk_starts-1], "or") and (getAbility(words[atk_starts-2]) ~= "") then
  					atk_stat = "";
  					atk_starts = atk_starts - 2;
  				end
  			end
  			
  			local atk_ends = i;
  			if StringManager.isWord(words[atk_ends + 1], ".") then
  				atk_ends = atk_ends + 1;
  			end

  			local atk_defense = {};
  			local j = atk_ends + 1;
  			while words[j] do
  				if StringManager.isWord(words[j], "the") then
  					-- SKIP - ONE OFF (PHB - Dispel Magic)
  				else
					local local_defense = getDefense(words[j]);
					if local_defense == "" then
						break;
					end
					table.insert(atk_defense, local_defense);
				end

  				j = j + 1;
  			end
  			atk_ends = j - 1;

			-- If the defense checks out, then we have a viable attack clause
			if #atk_defense > 0 then
				local atk_statarray = {};
				if atk_stat ~= "" then
					table.insert(atk_statarray, atk_stat);
				end
				
				local rAttack = {};
				rAttack.startindex = atk_starts;
				rAttack.endindex = atk_ends;
				if #atk_defense == 1 then
					rAttack.defense = atk_defense[1];
				else
					rAttack.defense = "";
				end
				rAttack.clauses = { { stat = atk_statarray, mod = atk_bonus } } ;
				
				table.insert(attacks, rAttack);
				
				if rAttack.clauses[1].mod ~= 0 then
					bAttackContinuationAllowed = true;
				end
				
				i = atk_ends;
			end

  		elseif bAttackContinuationAllowed and StringManager.isWord(words[i], "increase") and 
  				StringManager.isWord(words[i+1], "to") then
  			local nStartAttack = i;
  			local j = i + 2;
  			local bContinuation = true;
  			while bContinuation do
  				bContinuation = false;
  				
  				if StringManager.isNumberString(words[j]) then
					local rAttack = {};
					rAttack.startindex = nStartAttack;
					rAttack.endindex = j;
					if StringManager.isWord(words[rAttack.endindex + 1], "bonus") then
						rAttack.endindex = rAttack.endindex + 1;
					end
					rAttack.defense = attacks[#attacks].defense;
					
					local aAttackStat = attacks[#attacks].clauses[1].stat;
					local nAttackBonus = tonumber(words[j]) or 0;
					rAttack.clauses = { { stat = aAttackStat, mod = nAttackBonus } };
					
					-- CHECK FOR FOLLOW-ON CONTINUATIONS
					if StringManager.isWord(words[rAttack.endindex+1], "at") and 
							words[rAttack.endindex+2] and string.match(words[rAttack.endindex+2], "^%d?%d[st][th]$") and
							StringManager.isWord(words[rAttack.endindex+3], "level") then

						bContinuation = true;
						rAttack.endindex = rAttack.endindex + 3;

						nStartAttack = rAttack.endindex + 1;
						j = nStartAttack;
						if StringManager.isWord(words[j], "and") then
							j = j + 1;
						end
						if StringManager.isWord(words[j], "to") then
							j = j + 1;
						end
					end

					table.insert(attacks, rAttack);
					
					i = rAttack.endindex;
  				end
  			end
  		end
	end
	
	-- Return the set of clauses found in the string
	return attacks;
end

-- <dice> = [<#d#+#>, <#d#>, <#>]
-- <etype> = ["and", <energy type>]*
-- <statmod> = ["strength", "dexterity", "constitution", "intelligence", "wisdom", "charisma", "str", "dex", "con", "int", "wis", "cha"] ["modifier"]?
-- <mult> = <#>
--
-- Form [PHB]: <dice> <etype> damage
-- Form [PHB]: <dice> + <statmod> <etype> damage
-- Form [PHB]: <dice> + <statmod> + <statmod> <etype> damage
-- Form [PHB]: <mult>[W] <etype> damage
-- Form [PHB]: <mult>[W] + <statmod> <etype> damage
-- Form [PHB]: <mult>[W] + <statmod> + <statmod> <etype> damage
-- Form [PHB2]: <mult>[W] + <dice> + <statmod> <etype> damage
-- Form [MM]: <dice> <etype> damage
-- Form [MM]: <dice> <etype> damage crit <dice>
-- Form [MM2]: <dice> crit <dice> <etype> damage
function parseDamageClause(words, damage_index, inclusive_flag)

	-- SETUP VARIABLES
	local dmg_startindex = damage_index;
	local dmg_endindex = 0;
	if inclusive_flag then
		dmg_endindex = damage_index;
	else
		dmg_endindex = damage_index - 1;
	end

	local dmg_dice = {};
	local dmg_weaponmult = 0;
	local dmg_abilities = {};
	local dmg_types = {};

	local isweapon = false;
	local numberflag = false;
	local extraflag = false;
	local plusflag = false;
	local ongoingflag = false;
	local orflag = false;

	-- Go backwards through words to find the parts of the damage clause
	local j = damage_index - 1;
	while j > 0 do
		local skip_flag = StringManager.isWord(words[j], {"and", "modifier", "your", "as", "critical"});
		
		-- Skip null words
		if skip_flag then
		
		elseif StringManager.isWord(words[j], "or") then
			orflag = true;
		
		-- Plus or minus sign will reset number flag
		elseif StringManager.isWord(words[j], {"+", "-"}) then
			numberflag = false;

		-- Check for weapon damage clauses
		elseif StringManager.isWord(words[j], "w") then
			isweapon = true;

		-- Check to see if this is crit damage
		-- If so, then ignore since it should have already been processed
		elseif StringManager.isWord(words[j], "crit") then
			return damage_index, nil;
		
		-- Check to see if this is plus damage
		elseif StringManager.isWord(words[j], "plus") then
			if ongoingflag then
				break;
			end
			plusflag = true;
			extraflag = false;
		
		-- Check to see if this is extra damage
		elseif StringManager.isWord(words[j], {"extra", "additional"}) then
			extraflag = true;
		
		-- Check to see if this is ongoing damage
		elseif StringManager.isWord(words[j], "ongoing") then
			ongoingflag = true;
		
		-- Check for damage types
		elseif StringManager.isWord(words[j], DataCommon.dmgtypes) then
			table.insert(dmg_types, 1, words[j]);

		-- Check for dice string
		elseif StringManager.isDiceString(words[j]) then

			local dicestr = words[j];

			-- Not allowed to have 2 number/dice phrases in a row without a '+' or '-' in-between
			if numberflag then
				break;
			end
			numberflag = true;

			-- Catch weapon multiplier if we just found a [W]
			if isweapon then
				-- Check for leading plus sign
				if string.sub(dicestr, 1, 1) == "+" then
					extraflag = true;
				end
				dmg_weaponmult = tonumber(dicestr) or 0;
			-- Otherwise, we have a dice string or numbers
			else
				-- Strip leading plus sign
				if string.sub(dicestr, 1, 1) == "+" then
					dicestr = string.sub(dicestr, 2);
					extraflag = true;
				end

				-- Handle negatives
				if StringManager.isWord(words[j-1], "-") then
					dicestr = "-" .. dicestr;
					j = j - 1;
				end

				-- Add to the dice string list
				table.insert(dmg_dice, 1, dicestr);
			end

		-- Otherwise process the damage clause
		else
			-- Look for ability keywords
			local abilitystr = getAbility(words[j]);
			if abilitystr ~= "" then
				if StringManager.isWord(words[j-1], "half") then
					abilitystr = "half" .. abilitystr;
					j = j - 1;
				end
				table.insert(dmg_abilities, 1, abilitystr);

			-- Otherwise, we're done searching backwards
			else
				if StringManager.isWord(words[j+1], "or") then
					orflag = false;
				end
				break;
			end
			
		end
		
		-- UPDATE START POSITION
		if skip_flag then
		elseif StringManager.isWord(words[j], "or") then
		else
			dmg_startindex = j;
		end

		-- DECREMENT INDEX
		j = j - 1;
	end

	-- Check for a critical clause
	-- Does not support weapon clauses, or ability clauses (only for fixed dice + modifier crit clauses)
	local crit_index = nil;
	local crit_dice = {};
	if (not StringManager.isWord(words[damage_index], "plus")) and 
			(StringManager.isWord(words[damage_index], "crit") or StringManager.isWord(words[damage_index+1], "crit")) then
		local crit_types = {};
		local critj = damage_index + 1;
		local crit_index = critj;
		local critorflag = false;
		numberflag = false;
		if StringManager.isWord(words[critj], "crit") then
			critj = critj + 1;
		end
		while words[critj] do
			-- Skip null words
			if StringManager.isWord(words[critj], "and") then
			
			elseif StringManager.isWord(words[critj], "or") then
				critorflag = true;
			
			-- Plus or minus sign will reset number flag
			elseif StringManager.isWord(words[critj], {"+", "-"}) then
				numberflag = false;

			-- Look for damage types
			elseif StringManager.isWord(words[critj], DataCommon.dmgtypes) then
				table.insert(crit_types, 1, words[critj]);
				crit_index = critj;

			-- Basic dice string checks
			elseif StringManager.isDiceString(words[critj]) then

				local dicestr = words[critj];

				-- Not allowed to have 2 number/dice phrases in a row without a '+' or '-' in-between
				if numberflag then
					break;
				end
				numberflag = true;

				-- Strip leading plus sign
				if string.sub(dicestr, 1, 1) == "+" then
					dicestr = string.sub(dicestr, 2);
				end

				-- Handle negatives
				if (critj > 2) and StringManager.isWord(words[critj], "-") then
					dicestr = "-" .. dicestr;
				end

				-- Add to the dice string list
				table.insert(crit_dice, 1, dicestr);
				crit_index = critj;

			else			
				if StringManager.isWord(words[critj], "damage") then
					crit_index = critj;
				end
				if StringManager.isWord(words[critj - 1], "or") then
					critorflag = false;
				end
				break;
			end
			
			-- Increment word counter
			critj = critj + 1;
		end
		
		-- Check to see if we found a crit phrase
		if #crit_dice > 0 then
			-- Set the index to the new value
			damage_index = critj - 1;
			dmg_endindex = crit_index;
			if critorflag then
				orflag = true;
			end
		
			-- Add the crit damage type to the existing damage type
			-- NOTE: We assume that damage type is the same for crit and regular damage
			-- Assumption holds true for crit phrases in MM and MM2
			for k,v in pairs(crit_types) do
				local found = false;
				for k2, v2 in pairs(dmg_types) do
					if v == v2 then
						found = true;
					end
				end
				if not found then
					table.insert(dmg_types, 1, v);
				end
			end
		end
	end

	-- Clear damage if we found a damage choice clause
	if orflag then
		dmg_types = {};
		dmg_abilities = {};
	end

	-- ONE OFF (PHB - ASTRAL VIBRANCE)
	if StringManager.isWord(words[dmg_endindex+1], "of") and StringManager.isWord(words[dmg_endindex+2], "your") and
			StringManager.isWord(words[dmg_endindex+3], "chosen") and StringManager.isWord(words[dmg_endindex+4], "type") then
		dmg_endindex = dmg_endindex + 4;
	end
	
	-- Check for "equal to" damage clauses
	local equal_index = nil;
	if #dmg_dice == 0 and #dmg_abilities == 0 then
		if StringManager.isWord(words[dmg_endindex+1], "equal") and StringManager.isWord(words[dmg_endindex+2], "to") then
			local rValue = parseValuePhrase(words, dmg_endindex + 3);
			if rValue then
				equal_index = rValue.nIndex;
				dmg_endindex = rValue.nIndex;
				dmg_dice = rValue.aDice;
				dmg_abilities = rValue.aAbilities;
			end
		end
	end

	-- Make sure we have a damage clause, and insert it into table
	if #dmg_dice > 0 or dmg_weaponmult ~= 0 or #dmg_abilities > 0 then

		local dmgtypestr = table.concat(dmg_types, ",");
		local dicestr = table.concat(dmg_dice, "+");
		dicestr = string.gsub(dicestr,"%+%-","-");
		local critdicestr = table.concat(crit_dice, "+");
		critdicestr = string.gsub(critdicestr,"%+%-","-");

		if crit_index then
			damage_index = crit_index;
		end
		if equal_index then
			damage_index = equal_index;
		end

		local rDamage = {};
		rDamage.startindex = dmg_startindex;
		rDamage.endindex = dmg_endindex;
		if plusflag then
			rDamage.plusflag = true;
		end
		if extraflag then
			rDamage.extraflag = true;
		end
		if ongoingflag then
			rDamage.ongoingflag = true;
		end

		local rDamageClause = {};
		rDamageClause.basemult = dmg_weaponmult;
		rDamageClause.dicestr = dicestr;
		rDamageClause.critdicestr = critdicestr;
		rDamageClause.stat = dmg_abilities;
		rDamageClause.subtype = dmgtypestr;

		rDamage.clauses = { rDamageClause };

		return damage_index, rDamage;
	end
	
	-- Return null
	return damage_index, nil;
end

function parseDamagesAdd(damages, rDamage)
	
	-- VALIDATE THAT WE HAVE A DAMAGE CLAUSE TO ADD
	if not rDamage then
		return;
	end
	
	-- VALIDATE THAT THIS IS NOT AN EFFECT (ONGOING OR EXTRA)
	if rDamage.ongoingflag then
		return;
	end
	
	-- ONE OFF (PHB - Dire Radiance, Hellish Rebuke)
	if rDamage.extraflag then
		if (rDamage.clauses[1].dicestr == "") or (#(rDamage.clauses[1].stat) == 0) then
			return;
		end
	end
	
	-- CHECK FOR A COMBINED DAMAGE CLAUSE
	if rDamage.plusflag and (#damages > 0) then
		for k, v in pairs(rDamage.clauses) do
			table.insert(damages[#damages].clauses, v);
		end
		damages[#damages].endindex = rDamage.endindex;
		return;
	end
	
	-- OTHERWISE, JUST ADD AS A STANDARD DAMAGE CLAUSE
	table.insert(damages, rDamage);
end

function parseDamageContinuation(damages, words, nIndex)

	local bContinuation = true;

	while bContinuation do
		bContinuation = false;

		-- LOOK FOR DAMAGE VALUE
		local rDamage = nil;
		local nStartDamage = nIndex;
		local nBaseMult = nil;
		if StringManager.isNumberString(words[nIndex]) and StringManager.isWord(words[nIndex+1], "w") then
			nBaseMult = tonumber(words[nIndex]);
			nIndex = nIndex + 2;
		end
		local rValue = parseValuePhrase(words, nIndex);
		if nBaseMult or rValue then
			
			rDamage = {};
			rDamage.startindex = nStartDamage;
			if rValue then
				rDamage.endindex = rValue.nIndex;
			else
				rDamage.endindex = nStartDamage + 1;
			end

			local rDamageClause = {};
			if nBaseMult then
				rDamageClause.basemult = nBaseMult;
			else
				rDamageClause.basemult = 0;
			end
			if rValue then
				rDamageClause.dicestr = StringManager.convertDiceWordArrayToDiceString(rValue.aDice);
				if rValue.bOrFlag then
					rDamageClause.stat = {};
				else
					rDamageClause.stat = rValue.aAbilities;
				end
			else
				rDamageClause.dicestr = "";
				rDamageClause.stat = {};
			end
			rDamageClause.critdicestr = "";
			rDamage.clauses = { rDamageClause };

			if rValue then
				nIndex = rValue.nIndex + 1;
			end
		end

		if rDamage then
			-- CHECK FOR CONTINUATION DAMAGE TYPE
			if (#damages > 0) and (damages[#damages].clauses[1].subtype ~= "") then
				rDamage.clauses[1].subtype = damages[#damages].clauses[1].subtype;
			end
		
			-- ONE OFF (PHB - Careful Attack)
			if StringManager.isWord(words[nIndex], { "melee", "ranged" }) then
				bContinuation = true;
				rDamage.endindex = rDamage.endindex + 1;
				nIndex = nIndex + 1;
				if StringManager.isWord(words[nIndex], "or") then
					nIndex = nIndex + 1;
				end

			-- ONE OFF (PHB - Riposte Strike)
			elseif StringManager.isWord(words[nIndex], "and") and StringManager.isWord(words[nIndex+1], "riposte") and
					StringManager.isWord(words[nIndex+2], "to") then
				bContinuation = true;
				nIndex = nIndex + 3;
			end

			-- CHECK FOR FOLLOW-ON CONTINUATIONS
			if StringManager.isWord(words[rDamage.endindex+1], "at") and 
					words[rDamage.endindex+2] and string.match(words[rDamage.endindex+2], "^%d?%d[st][th]$") then
				
				bContinuation = true;
				if StringManager.isWord(words[rDamage.endindex+3], "level") then
					rDamage.endindex = rDamage.endindex + 3;
				else
					rDamage.endindex = rDamage.endindex + 2;
				end

				nIndex = rDamage.endindex + 1;
				if StringManager.isWord(words[nIndex], "and") then
					nIndex = nIndex + 1;
				end
				if StringManager.isWord(words[nIndex], "to") then
					nIndex = nIndex + 1;
				end
			end

			-- ADD TO DAMAGES ARRAY
			parseDamagesAdd(damages, rDamage);
		end
	end
	
	-- RESULTS
	return nIndex;
end

function parseDamages(words)
	-- SETUP
	local damages = {};

  	-- LOOK FOR DAMAGE WORDS
  	local i = 1;
  	while words[i] do
		
		-- MAIN TRIGGER ("damage")
		if StringManager.isWord(words[i], "damage") then
			if StringManager.isWord(words[i+1], "increases") and StringManager.isWord(words[i+2], "to") then
				i = parseDamageContinuation(damages, words, i + 3);
			else
				i, rDamage = parseDamageClause(words, i, true);
				if rDamage and StringManager.isWord(words[rDamage.endindex+1], "at") and 
						words[rDamage.endindex+2] and string.match(words[rDamage.endindex+2], "^%d?%d[st][th]$") and
						StringManager.isWord(words[rDamage.endindex+3], "level") then
					rDamage.endindex = rDamage.endindex + 3;
					i = i + 3;
				end
				parseDamagesAdd(damages, rDamage);
			end
		-- ALTERNATE TRIGGER ("plus")
		elseif StringManager.isWord(words[i], "plus") and StringManager.isNumberString(words[i-1]) then
			i, rDamage = parseDamageClause(words, i);
			parseDamagesAdd(damages, rDamage);
		-- ALTERNATE TRIGGER ("crit")
		elseif StringManager.isWord(words[i], "crit") then
			i, rDamage = parseDamageClause(words, i);
			parseDamagesAdd(damages, rDamage);
		-- CONTINUATION TRIGGER ("increase damage to")
		elseif StringManager.isWord(words[i], "increase") then
			local j = i + 1;
			if StringManager.isWord(words[j], "the") then
				j = j + 1;
			end
			if StringManager.isWord(words[j], "damage") and StringManager.isWord(words[j+1], "and") and
					StringManager.isWord(words[j+2], "extra") then
				j = j + 3;
			end
			if StringManager.isWord(words[j], "damage") and StringManager.isWord(words[j+1], "to") then
				i = parseDamageContinuation(damages, words, j + 2);
			end
  		end
		
		-- INCREMENT
		i = i + 1;
	end	

	-- RESULTS
	return damages;
end

function parseHeals(words)
	if PremiumPowersManager then
		return PremiumPowersManager.parseHeals(words);
	end
	
	return {};
end

function parseEffects(words, sCreatureName, nodePower)
	if PremiumPowersManager then
		return PremiumPowersManager.parseEffects(words, sCreatureName, nodePower);
	end
	
	return {};
end

-- SEPERATE OUT PERIODS, COLONS AND SEMICOLONS
function parseHelper(s, words, words_stats)
  	-- SETUP
  	local final_words = {};
  	local final_words_stats = {};
  	
  	-- SEPARATE WORDS ENDING IN PERIODS, COLONS AND SEMICOLONS
  	for i = 1, #words do
		local nSpecialChar = string.find(words[i], "[%.:;]");
		if nSpecialChar then
			local sWord = words[i];
			local nStartPos = words_stats[i].startpos;
			while nSpecialChar do
				if nSpecialChar > 1 then
					table.insert(final_words, string.sub(sWord, 1, nSpecialChar - 1));
					table.insert(final_words_stats, {startpos = nStartPos, endpos = nStartPos + nSpecialChar - 1});
				end
				
				table.insert(final_words, string.sub(sWord, nSpecialChar, nSpecialChar));
				table.insert(final_words_stats, {startpos = nStartPos + nSpecialChar - 1, endpos = nStartPos + nSpecialChar});
				
				nStartPos = nStartPos + nSpecialChar;
				sWord = string.sub(sWord, nSpecialChar + 1);
				
				nSpecialChar = string.find(sWord, "[%.:;]");
			end
			if string.len(sWord) > 0 then
				table.insert(final_words, sWord);
				table.insert(final_words_stats, {startpos = nStartPos, endpos = words_stats[i].endpos});
			end
		else
			table.insert(final_words, words[i]);
			table.insert(final_words_stats, words_stats[i]);
		end
  	end
  	
	-- RESULTS
	return final_words, final_words_stats;
end

function consolidationHelper(aMasterAbilities, aWordStats, sAbilityType, aNewAbilities)
	-- ITERATE THROUGH NEW ABILITIES
	for i = 1, #aNewAbilities do

		-- ADD TYPE
		aNewAbilities[i].type = sAbilityType;

		-- CONVERT WORD INDICES TO CHARACTER POSITIONS
		aNewAbilities[i].startpos = aWordStats[aNewAbilities[i].startindex].startpos;
		aNewAbilities[i].endpos = aWordStats[aNewAbilities[i].endindex].endpos;
		aNewAbilities[i].startindex = nil;
		aNewAbilities[i].endindex = nil;

		-- ADD TO MASTER ABILITIES LIST
		table.insert(aMasterAbilities, aNewAbilities[i]);
	end
end

function parsePowerDescription(nodePower, sCreatureName, sFieldName)
	-- VALIDATE
	if not sFieldName then
		sFieldName = "shortdescription";
	end
	
	-- GET THE POWER DESCRIPTION
	local s = NodeManager.get(nodePower, sFieldName, "");
	
	-- GET RID OF SOME PROBLEM CHARACTERS, AND MAKE LOWERCASE
	local sLocal = string.gsub(s, "’", "'");
	sLocal = string.gsub(sLocal, "–", "-");
	sLocal = string.lower(sLocal);
	
	-- PARSE THE WORDS
	local aWords, aWordStats = StringManager.parseWords(sLocal, ".;");
	
	-- ADD/SEPARATE MARKERS FOR END OF SENTENCE, END OF CLAUSE AND CLAUSE LABEL SEPARATORS
	aWords, aWordStats = parseHelper(s, aWords, aWordStats);
	
	-- BUILD MASTER LIST OF ALL POWER ABILITIES
	local aMasterAbilities = {};
	consolidationHelper(aMasterAbilities, aWordStats, "attack", parseAttacks(aWords));
	consolidationHelper(aMasterAbilities, aWordStats, "damage", parseDamages(aWords));
	consolidationHelper(aMasterAbilities, aWordStats, "heal", parseHeals(aWords));
	consolidationHelper(aMasterAbilities, aWordStats, "effect", parseEffects(aWords, sCreatureName, nodePower));
	
	-- SORT THE ABILITIES
	table.sort(aMasterAbilities, function(a,b) return a.startpos < b.startpos end)
	
	-- RESULTS
	return aMasterAbilities;
end
